/**
 * Copyright (c) <2010>, <SUNNY GUPTA>
 * All rights reserved.

 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   * Neither the name of the <ORGANIZATION> nor the
 *     names of its contributors may be used to endorse or promote products
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL <SUNNY GUPTA> BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.gwtp.investmentadvisor.server.authentication.dao;

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;

import com.google.inject.Inject;
import com.googlecode.objectify.Objectify;
import com.googlecode.objectify.Query;
import com.gwtp.investmentadvisor.server.authentication.AuthenticationManager;
import com.gwtp.investmentadvisor.server.authentication.annotation.MaxSessionIdGenerationAttempts;
import com.gwtp.investmentadvisor.server.authentication.annotation.SessionValidityDuration;
import com.gwtp.investmentadvisor.server.authentication.entities.SessionIDToken;
import com.gwtp.investmentadvisor.server.authentication.entities.UserAccount;
import com.gwtp.investmentadvisor.server.authentication.exception.SessionIDGenerationFailedException;
import com.gwtp.investmentadvisor.server.authentication.exception.UserLogoutFailedException;
import com.gwtp.investmentadvisor.server.core.datastore.ObjectifyGenericDAO;
import com.gwtp.investmentadvisor.server.core.security.CryptographyHelper;
import com.gwtp.investmentadvisor.shared.entities.UserRole;
import com.gwtp.investmentadvisor.shared.utilities.Encoder;

/**
 * <p>Data Access Object / Business Logic for {@link UserAccount} entity.</p>
 * <p>Most of Data Access is handled by Objectify and this class mostly does Business Logic functions</p>
 * @author Sunny Gupta
 *
 */
public class UserAccountDAO extends ObjectifyGenericDAO<UserAccount> implements AuthenticationManager {

  private final long sessionValidityDurationInMs;
  private final int maxSessionIdGenerationAttempts;

  @Inject
  public UserAccountDAO(@SessionValidityDuration long sessionValidityDuration,
      @MaxSessionIdGenerationAttempts int maxSessionIdGenerationAttempts) {
    // Convert to milliseconds
    this.sessionValidityDurationInMs = 1000 * 60 * 60 * 24 * sessionValidityDuration;
    this.maxSessionIdGenerationAttempts = maxSessionIdGenerationAttempts;
  }
  
  /* (non-Javadoc)
   * @see com.gwtp.investmentadvisor.server.authentication.AuthenticationManager#isUsernameAvailabile(java.lang.String)
   */
  @Override
  public boolean isUsernameAvailabile(String username) {
    // Return true (as username is then available) when ID not found, else false
    return (super.ofy().find(UserAccount.class, username) == null) ? true : false;
  }

  /* (non-Javadoc)
   * @see com.gwtp.investmentadvisor.server.authentication.AuthenticationManager#authenticateUser(java.lang.String, java.lang.String)
   */
  @Override
  public boolean authenticateUser(String username, String password) {
    // Try to obtain user
    UserAccount user = super.ofy().find(UserAccount.class, username);
    if (user != null) {
      // Compare hash
      return CryptographyHelper.checkPassword(password, user.getPasswordHash());
    }
    // User not found, failed
    return false;
  }

  /* (non-Javadoc)
   * @see com.gwtp.investmentadvisor.server.authentication.AuthenticationManager#getSessionID(java.lang.String)
   */
  @Override
  public String getSessionID(String username)
      throws SessionIDGenerationFailedException {
    // First check for existing token
    Query<SessionIDToken> query = super.ofy().query(SessionIDToken.class).filter("username", username);
    // Check for null
    if (query == null) {
      throw new SessionIDGenerationFailedException("Query returned null");
    }
    
    int count = query.countAll();
    if (count > 1) {
      String errorMsg = "Multiple sessionID found for " + username + ": %n";
      for (SessionIDToken token : query) {
        errorMsg += token.toString() + "%n";
      }
      throw new SessionIDGenerationFailedException(errorMsg);
    } else {
      SessionIDToken sessionID = query.get();
      // Check for null
      if (sessionID != null) {
        // Validate sessionID
        String sessionIDtext = sessionID.getSessionID();
        UserAccount userAccount = validateSessionID(sessionIDtext);
        // Again confirm username
        if (userAccount != null && userAccount.getUserID().equals(username)) {
          // Validated
          return sessionIDtext;
        }
      }
      
      // Not found, generate
      return generateNewSessionID(username, sessionID);
    }
  }

  private String generateNewSessionID(String username, SessionIDToken oldSessionID) throws SessionIDGenerationFailedException {   
    // get a date to represent "now"
    Date now = new Date();

    // now get "expiration"
    Date expiration = new Date(System.currentTimeMillis() + sessionValidityDurationInMs);

    // generate sessionID by scrambling
    DateFormat formatter = SimpleDateFormat.getDateTimeInstance();
    String sessionIDhash = Encoder.scrambleString(username + formatter.format(now));

    // Check if sessionID exists already
    int numTrials = maxSessionIdGenerationAttempts;
    while (super.ofy().find(SessionIDToken.class, sessionIDhash) != null) {
      // Modify sessionIDhash
      sessionIDhash = CryptographyHelper.generateHash(sessionIDhash);
      numTrials--;
      if (numTrials == 0) {
        throw new SessionIDGenerationFailedException(
            "Too many attempts to regenerate session ID: username=" + username);
      }
    }

    // Update datastore
    SessionIDToken newSessionID = new SessionIDToken();
    newSessionID.setSessionID(sessionIDhash);
    newSessionID.setUsername(username);
    newSessionID.setExpiration(expiration);

    // Start transaction for changing entities
    Objectify ofy = super.fact().beginTransaction();

    // delete old and add new
    try {
      // Delete if old exists
      if (oldSessionID != null) {
        ofy.delete(oldSessionID);
      }

      // Add new token
      ofy.put(newSessionID);

      // Commit
      ofy.getTxn().commit();

      // return
      return sessionIDhash;
    } catch (Exception e) {
      if (ofy.getTxn().isActive()) {
        ofy.getTxn().rollback();
      }
      // Txn Failed
      throw new SessionIDGenerationFailedException("Internal error during transaction commit", e);
    }
  }
  
  /* (non-Javadoc)
   * @see com.gwtp.investmentadvisor.server.authentication.AuthenticationManager#validateSessionID(java.lang.String)
   */
  @Override
  public UserAccount validateSessionID(String sessionID) {
    SessionIDToken token = super.ofy().find(SessionIDToken.class, sessionID);
    if (token != null) {
      // Check timestamp
      Date now = new Date();
      if (token.getExpiration().after(now)) {
        return super.ofy().find(UserAccount.class, token.getUsername());
      }
    }
    // Not found
    return null;
  }

  /* (non-Javadoc)
   * @see com.gwtp.investmentadvisor.server.authentication.AuthenticationManager#getUserRoles(java.lang.String)
   */
  @Override
  public List<UserRole> getUserRoles(String username) {
    // Try to obtain user
    UserAccount user = super.ofy().find(UserAccount.class, username);
    if (user != null) {
      return user.getUserRoles();
    } else {  
      // User not found, failed
      return null;
    }
  }

  /* (non-Javadoc)
   * @see com.gwtp.investmentadvisor.server.authentication.AuthenticationManager#logoutUser(java.lang.String)
   */
  @Override
  public boolean logoutUser(String username) throws UserLogoutFailedException {
    // Start transaction for changing entities
    Objectify ofy = super.fact().beginTransaction();
    
    try {
      // Look for session IDs corresponding to username
      Query<SessionIDToken> query = super.ofy().query(SessionIDToken.class).filter("username", username);
      // Check for null
      if (query == null) {
        throw new UserLogoutFailedException("Query returned null");
      }
      
      // Check for size
      if (query.countAll() == 0) {
        // No user session, so cannot log out
        
        if (ofy.getTxn().isActive()) {
          ofy.getTxn().rollback();
        }
        
        return false;
      }
      
      // Delete all tokens
      ofy.delete(query);
      
      // Commit
      ofy.getTxn().commit();
        
      return true;
    } catch (Exception e) {
      if (ofy.getTxn().isActive()) {
        ofy.getTxn().rollback();
      }
      // Txn Failed
      throw new UserLogoutFailedException("Internal error during transaction commit", e);
    }
  }

  /* (non-Javadoc)
   * @see com.gwtp.investmentadvisor.server.authentication.AuthenticationManager#addNewUser(com.gwtp.investmentadvisor.server.authentication.entities.UserAccount)
   */
  @Override
  public void addNewUser(UserAccount newUser) {
    addNew(newUser);
  }
}
